using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;


namespace LightCAD.Three
{
    public class FrenetFrame
    {
        public ListEx<Vector3> tangents;
        public ListEx<Vector3> normals;
        public ListEx<Vector3> binormals;
    }
    public abstract class Curve<TVec> where TVec : Vector, new()
    {
        #region Properties

        public string type;
        public bool needsUpdate;
        public int arcLengthDivisions;
        public ListEx<double> cacheArcLengths;

        #endregion

        #region constructor
        public Curve()
        {
            this.type = "Curve";
            this.arcLengthDivisions = 200;
        }
        #endregion

        #region methods
        public virtual TVec getPoint(double t, TVec optionalTarget = default(TVec))
        {
            console.warn("THREE.Curve: .getPoint() not implemented.");
            return default;
        }

        public virtual TVec getPointAt(double u, TVec optionalTarget = default(TVec))
        {
            var t = this.getUtoTmapping(u);
            return this.getPoint(t, optionalTarget);

        }
        public virtual ListEx<TVec> getPoints(int divisions = 5)
        {
            var points = new ListEx<TVec>();
            for (int d = 0; d <= divisions; d++)
            {
                points.Push(this.getPoint(d / (double)divisions));
            }
            return points;
        }
        public virtual ListEx<TVec> getSpacedPoints(int divisions = 5)
        {
            var points = new ListEx<TVec>();
            for (int d = 0; d <= divisions; d++)
            {
                points.Push(this.getPointAt(d / (double)divisions));
            }
            return points;
        }
        public virtual double getLength()
        {
            var lengths = this.getLengths();
            return lengths[lengths.Length - 1];
        }

        public virtual ListEx<double> getLengths(int? divisions = null)
        {

            if (divisions == null) divisions = this.arcLengthDivisions;

            if (this.cacheArcLengths != null &&
                (this.cacheArcLengths.Length == divisions + 1) &&
                !this.needsUpdate)
            {
                return this.cacheArcLengths;
            }

            this.needsUpdate = false;

            var cache = new ListEx<double>();
            TVec current, last = this.getPoint(0);
            int p;
            double sum = 0;

            cache.Push(0);

            for (p = 1; p <= divisions; p++)
            {

                current = this.getPoint(p / (double)divisions.Value);
                if (current is Vector2)
                    sum += (current as Vector2).DistanceTo(last as Vector2);
                else if (current is Vector3)
                    sum += (current as Vector3).DistanceTo(last as Vector3);
                cache.Push(sum);
                last = current;

            }

            this.cacheArcLengths = cache;

            return cache; // { sums = cache, sum = sum }; Sum is in the last element.
        }
        public virtual void updateArcLengths()
        {
            this.needsUpdate = true;
            this.getLengths();
        }
        public virtual double getUtoTmapping(double u, double? distance = null)
        {
            var arcLengths = this.getLengths();
            var i = 0;
            var il = arcLengths.Length;
            double targetArcLength; // The targeted u distance value to  else {
            if (distance != null)
            {
                targetArcLength = distance.Value;
            }
            else
            {
                targetArcLength = u * arcLengths[il - 1];
            }
            // binary search for the index with largest value smaller than target u distance
            int low = 0, high = il - 1;
            double comparison;
            while (low <= high)
            {
                i = (int)Math.Floor(low + (high - low) / 2f); // less likely to overflow, though probably not issue here, JS doesn"t really have integers, all numbers are floats
                comparison = arcLengths[i] - targetArcLength;
                if (comparison < 0)
                {
                    low = i + 1;
                }
                else if (comparison > 0)
                {
                    high = i - 1;
                }
                else
                {
                    high = i;
                    break;
                    // DONE
                }
            }
            i = high;
            if (arcLengths[i] == targetArcLength)
            {
                return (double)i / (il - 1);
            }
            // we could get finer grain at lengths, or use simple interpolation between two points
            var lengthBefore = arcLengths[i];
            var lengthAfter = arcLengths[i + 1];
            var segmentLength = lengthAfter - lengthBefore;
            // determine where we are between the "before" and "after" points
            var segmentFraction = (targetArcLength - lengthBefore) / segmentLength;
            // add that fractional amount to t
            var t = (i + segmentFraction) / (il - 1);
            return t;
        }
        public virtual TVec getTangent(double t = 0, TVec optionalTarget = default(TVec))
        {
            var delta = 0.0001;
            var t1 = t - delta;
            var t2 = t + delta;
            // Capping in case of danger
            if (t1 < 0) t1 = 0;
            if (t2 > 1) t2 = 1;
            var pt1 = this.getPoint(t1);
            var pt2 = this.getPoint(t2);
            if (pt1 is Vector2)
            {
                var v2 = new Vector2().Copy(pt2 as Vector2).Sub(pt1 as Vector2).Normalize();
                if (optionalTarget != null)
                {
                    (optionalTarget as Vector2).Copy(v2);
                }
                return v2 as TVec;
            }
            else
            {
                var v3 = new Vector3().Copy(pt2 as Vector3).Sub(pt1 as Vector3).Normalize();
                if (optionalTarget != null)
                {
                    (optionalTarget as Vector3).Copy(v3);
                }
                return v3 as TVec;
            }
        }
        public virtual TVec getTangentAt(double u, TVec optionalTarget = default(TVec))
        {
            var t = this.getUtoTmapping(u);
            return this.getTangent(t, optionalTarget);
        }
        public FrenetFrame computeFrenetFrames(int segments, bool closed)
        {
            // see http://www.cs.indiana.edu/pub/techreports/TR425.pdf
            var normal = new Vector3();
            var tangents = new ListEx<Vector3>();
            var normals = new ListEx<Vector3>();
            var binormals = new ListEx<Vector3>();
            var vec = new Vector3();
            var mat = new Matrix4();
            // compute the tangent vectors for each segment on the curve
            for (int i = 0; i <= segments; i++)
            {
                var u = (double)i / segments;
                var v = this.getTangentAt(u);
                if (v is Vector2)
                    tangents[i] = (v as Vector2).ToVector3();
                else
                    tangents[i] = (v as Vector3);
            }
            // select an initial normal vector perpendicular to the first tangent vector,
            // and in the direction of the minimum tangent xyz component
            normals[0] = new Vector3();
            binormals[0] = new Vector3();
            var min = MathEx.MAX_VALUE;
            var tx = Math.Abs(tangents[0].X);
            var ty = Math.Abs(tangents[0].Y);
            var tz = Math.Abs(tangents[0].Z);
            if (tx <= min)
            {
                min = tx;
                normal.Set(1, 0, 0);
            }
            if (ty <= min)
            {
                min = ty;
                normal.Set(0, 1, 0);
            }
            if (tz <= min)
            {
                normal.Set(0, 0, 1);
            }
            vec.CrossVectors(tangents[0], normal).Normalize();
            normals[0].CrossVectors(tangents[0], vec);
            binormals[0].CrossVectors(tangents[0], normals[0]);
            // compute the slowly-varying normal and binormal vectors for each segment on the curve
            for (int i = 1; i <= segments; i++)
            {
                normals[i] = normals[i - 1].Clone();
                binormals[i] = binormals[i - 1].Clone();
                vec.CrossVectors(tangents[i - 1], tangents[i]);
                if (vec.Length() > MathEx.EPSILON)
                {
                    vec.Normalize();
                    var theta = Math.Acos(MathEx.Clamp(tangents[i - 1].Dot(tangents[i]), -1, 1)); // clamp for floating pt errors
                    normals[i].ApplyMatrix4(mat.MakeRotationAxis(vec, theta));
                }
                binormals[i].CrossVectors(tangents[i], normals[i]);
            }
            // if the curve is closed, postprocess the vectors so the first and last normal vectors are the same
            if (closed)
            {
                var theta = Math.Acos(MathEx.Clamp(normals[0].Dot(normals[segments]), -1, 1));
                theta /= segments;
                if (tangents[0].Dot(vec.CrossVectors(normals[0], normals[segments])) > 0)
                {
                    theta = -theta;
                }
                for (int i = 1; i <= segments; i++)
                {
                    // twist a little...
                    normals[i].ApplyMatrix4(mat.MakeRotationAxis(tangents[i], theta * i));
                    binormals[i].CrossVectors(tangents[i], normals[i]);
                }
            }
            return new FrenetFrame
            {
                tangents = tangents,
                normals = normals,
                binormals = binormals

            };
        }
        public abstract Curve<TVec> clone();

        public virtual Curve<TVec> copy(Curve<TVec> source)
        {
            this.arcLengthDivisions = source.arcLengthDivisions;
            return this;
        }

        //public virtual Curve<TVec> fromJSON(JObject json)
        //{

        //    this.arcLengthDivisions = json.GetInt("arcLengthDivisions");
        //    return this;

        //}
        #endregion
    }
}
